//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import HummingbirdCore

/// Define media type of file
public struct MediaType: Sendable, CustomStringConvertible {
    /// general category
    public let type: Category
    /// exact kind of data specified
    public let subType: String
    /// optional parameter
    public let parameter: (name: String, value: String)?

    /// Initialize `MediaType`
    /// - Parameters:
    ///   - type: category
    ///   - subType: specific kind of data
    ///   - parameter: additional parameter
    public init(type: Category, subType: String = "*", parameter: (String, String)? = nil) {
        self.type = type
        self.subType = subType
        self.parameter = parameter
    }

    /// Construct `MediaType` from header value
    public init?(from header: String) {
        enum State: Equatable {
            case readingCategory
            case readingSubCategory
            case readingParameterKey
            case readingParameterValue(key: String)
            case finished
        }
        var parser = Parser(header)
        var state = State.readingCategory

        var category: Category?
        var subCategory: String?
        var parameter: (String, String)?

        while state != .finished {
            switch state {
            case .readingCategory:
                let categoryString = parser.read(while: { !Self.tSpecial.contains($0) }).string
                category = Category(rawValue: categoryString.lowercased())
                guard parser.current() == "/" else { return nil }
                parser.unsafeAdvance()
                state = .readingSubCategory

            case .readingSubCategory:
                subCategory = parser.read(while: { !Self.tSpecial.contains($0) }).string
                if parser.reachedEnd() {
                    state = .finished
                } else {
                    guard parser.current() == ";" else { return nil }
                    parser.unsafeAdvance()
                    parser.read(while: \.isWhitespace)
                    if parser.reachedEnd() {
                        state = .finished
                    } else {
                        state = .readingParameterKey
                    }
                }

            case .readingParameterKey:
                let key = parser.read(while: { !Self.tSpecial.contains($0) }).string
                guard parser.current() == "=" else { return nil }
                state = .readingParameterValue(key: key)
                parser.unsafeAdvance()

            case .readingParameterValue(let key):
                let value: String
                if parser.current() == "\"" {
                    parser.unsafeAdvance()
                    do {
                        value = try parser.read(until: "\"").string
                    } catch {
                        return nil
                    }
                } else {
                    value = parser.readUntilTheEnd().string
                }
                parameter = (key, value)
                state = .finished

            case .finished:
                break
            }
        }
        if let category,
            let subCategory
        {
            self.type = category
            self.subType = subCategory.lowercased()
            self.parameter = parameter
        } else {
            return nil
        }
    }

    /// Return media type with new parameter
    public func withParameter(name: String, value: String) -> MediaType {
        .init(type: self.type, subType: self.subType, parameter: (name, value))
    }

    /// Output
    public var description: String {
        if let parameter = self.parameter {
            return "\(self.type)/\(self.subType); \(parameter.name)=\(parameter.value)"
        } else {
            return "\(self.type)/\(self.subType)"
        }
    }

    /// Return if media type matches the input
    public func isType(_ type: MediaType) -> Bool {
        guard self.type ~= type.type,
            self.subType == type.subType || type.subType == "*"
        else {
            return false
        }
        if let parameter = type.parameter {
            return parameter.name == self.parameter?.name && parameter.value == self.parameter?.value
        } else {
            return true
        }
    }

    /// Get media type from a file extension
    /// - Parameter extension: file extension
    /// - Returns: media type
    public static func getMediaType(forExtension extension: String) -> MediaType? {
        getMediaType(forExtension: FileExtension(rawValue: `extension`))
    }

    /// Get media type from a file extension
    /// - Parameter extension: file extension
    /// - Returns: media type
    public static func getMediaType(forExtension extension: FileExtension) -> MediaType? {
        extensionMap[`extension`]
    }

    /// Media type categories
    public struct Category: Sendable, Equatable, RawRepresentable, CustomStringConvertible {
        internal enum Internal: String, Sendable {
            case application
            case audio
            case example
            case font
            case image
            case message
            case model
            case multipart
            case text
            case video
            case any
        }

        let value: Internal
        init(value: Internal) {
            self.value = value
        }

        public init?(rawValue: String) {
            guard let value = Internal(rawValue: rawValue) else { return nil }
            self.value = value
        }

        public var rawValue: String {
            self.value.rawValue
        }

        public var description: String {
            self.value.rawValue
        }

        static func ~= (_ lhs: Self, _ rhs: Self) -> Bool {
            switch (lhs.value, rhs.value) {
            case (.any, _), (_, .any):
                true
            default:
                lhs.value == rhs.value
            }
        }

        public static var application: Self { .init(value: .application) }
        public static var audio: Self { .init(value: .audio) }
        public static var example: Self { .init(value: .example) }
        public static var font: Self { .init(value: .font) }
        public static var image: Self { .init(value: .image) }
        public static var message: Self { .init(value: .message) }
        public static var model: Self { .init(value: .model) }
        public static var multipart: Self { .init(value: .multipart) }
        public static var text: Self { .init(value: .text) }
        public static var video: Self { .init(value: .video) }
        public static var any: Self { .init(value: .any) }
    }

    static let tSpecial = Set<Unicode.Scalar>(["(", ")", "<", ">", "@", ",", ";", ":", "\\", "\"", "/", "[", "]", "?", ".", "="])
}

extension MediaType: Codable {
    public init(from decoder: any Decoder) throws {
        let container = try decoder.singleValueContainer()
        let value = try container.decode(String.self)
        guard let value: Self = MediaType(from: value) else {
            throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Failed to decode MediaType"))
        }
        self = value
    }

    public func encode(to encoder: any Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(self.description)
    }
}

extension MediaType {
    // types
    public static var application: Self { .init(type: .application) }
    public static var audio: Self { .init(type: .audio) }
    public static var example: Self { .init(type: .example) }
    public static var font: Self { .init(type: .font) }
    public static var image: Self { .init(type: .image) }
    public static var message: Self { .init(type: .message) }
    public static var model: Self { .init(type: .model) }
    public static var multipart: Self { .init(type: .multipart) }
    public static var text: Self { .init(type: .text) }
    public static var video: Self { .init(type: .video) }

    // application files

    /// AbiWord document
    public static var applicationAbiWord: Self { .init(type: .application, subType: "x-abiword") }
    /// Archive document (multiple files embedded)
    public static var applicationArc: Self { .init(type: .application, subType: "x-freearc") }
    /// OGG
    public static var applicationOgg: Self { .init(type: .application, subType: "ogg") }
    /// Amazon Kindle eBook format
    public static var applicationAmzKindleEBook: Self { .init(type: .application, subType: "vnd.amazon.ebook") }
    /// Any kind of binary data
    public static var applicationBinary: Self { .init(type: .application, subType: "octet-stream") }
    /// BZip archive
    public static var applicationBzip: Self { .init(type: .application, subType: "x-bzip") }
    /// BZip2 archive
    public static var applicationBzip2: Self { .init(type: .application, subType: "x-bzip2") }
    /// C-Shell script
    public static var applicationCsh: Self { .init(type: .application, subType: "x-csh") }
    /// Microsoft Word
    public static var applicationMsword: Self { .init(type: .application, subType: "msword") }
    /// Microsoft Word (OpenXML)
    public static var applicationDocx: Self { .init(type: .application, subType: "vnd.openxmlformats-officedocument.wordprocessingml.document") }
    /// MS Embedded OpenType fonts
    public static var applicationEot: Self { .init(type: .application, subType: "vnd.ms-fontobject") }
    /// Electronic publication (EPUB)
    public static var applicationEpub: Self { .init(type: .application, subType: "application/epub+zip") }
    /// GZip Compressed Archive
    public static var applicationGzip: Self { .init(type: .application, subType: "gzip") }
    /// Java Archive (JAR)
    public static var applicationJar: Self { .init(type: .application, subType: "java-archive") }
    /// JSON format
    public static var applicationJson: Self { .init(type: .application, subType: "json") }
    /// JSON-LD format
    public static var applicationJsonLD: Self { .init(type: .application, subType: "ld+json") }
    /// Apple Installer Package
    public static var applicationMpkg: Self { .init(type: .application, subType: "application/vnd.apple.installer+xml") }
    /// URL encoded form data
    public static var applicationUrlEncoded: Self { .init(type: .application, subType: "x-www-form-urlencoded") }
    /// OpenDocument presentation document
    public static var applicationOdp: Self { .init(type: .application, subType: "vnd.oasis.opendocument.presentation") }
    /// OpenDocument spreadsheet document
    public static var applicationOds: Self { .init(type: .application, subType: "vnd.oasis.opendocument.spreadsheet") }
    /// OpenDocument text document
    public static var applicationOdt: Self { .init(type: .application, subType: "vnd.oasis.opendocument.text") }
    /// Adobe Portable Document Format
    public static var applicationPdf: Self { .init(type: .application, subType: "pdf") }
    /// Hypertext Preprocessor
    public static var applicationPhp: Self { .init(type: .application, subType: "x-httpd-php") }
    /// Microsoft PowerPoint
    public static var applicationPpt: Self { .init(type: .application, subType: "vnd.ms-powerpoint") }
    /// Microsoft PowerPoint (OpenXML)
    public static var applicationPptx: Self { .init(type: .application, subType: "vnd.openxmlformats-officedocument.presentationml.presentation") }
    /// RAR archive
    public static var applicationRar: Self { .init(type: .application, subType: "vnd.rar") }
    /// Rich Text Format (RTF)
    public static var applicationRtf: Self { .init(type: .application, subType: "rtf") }
    /// Bourne shell script
    public static var applicationSh: Self { .init(type: .application, subType: "x-sh") }
    /// Small web format (SWF) or Adobe Flash document
    public static var applicationSwf: Self { .init(type: .application, subType: "x-shockwave-flash") }
    /// Tape Archive (TAR)
    public static var applicationTar: Self { .init(type: .application, subType: "x-tar") }
    /// Microsoft Visio
    public static var applicationVsd: Self { .init(type: .application, subType: "vnd.visio") }
    /// WebAssembly
    public static var applicationWasm: Self { .init(type: .application, subType: "wasm") }
    /// XHTML
    public static var applicationXhtml: Self { .init(type: .application, subType: "xhtml+xml") }
    /// Microsoft Excel
    public static var applicationXls: Self { .init(type: .application, subType: "vnd.ms-excel") }
    /// Microsoft Excel (OpenXML)
    public static var applicationXlsx: Self { .init(type: .application, subType: "vnd.openxmlformats-officedocument.spreadsheetml.sheet") }
    /// XML
    public static var applicationXml: Self { .init(type: .application, subType: "xml") }
    /// ZIP archive
    public static var applicationZip: Self { .init(type: .application, subType: "zip") }
    /// 7-zip archive
    public static var application7z: Self { .init(type: .application, subType: "x-7z-compressed") }
    /// Manifest
    public static var applicationManifest: Self { .init(type: .application, subType: "manifest+json") }

    // text

    /// Text, (generally ASCII or ISO 8859-n)
    public static var textPlain: Self { .init(type: .text, subType: "plain") }
    /// iCalendar format
    public static var textICalendar: Self { .init(type: .text, subType: "calendar") }
    /// Cascading Style Sheets (CSS)
    public static var textCss: Self { .init(type: .text, subType: "css") }
    /// Comma-separated values (CSV)
    public static var textCsv: Self { .init(type: .text, subType: "csv") }
    /// HyperText Markup Language (HTML)
    public static var textHtml: Self { .init(type: .text, subType: "html") }
    /// JavaScript
    public static var textJavascript: Self { .init(type: .text, subType: "javascript") }

    // image formats

    /// Windows OS/2 Bitmap Graphics
    public static var imageBmp: Self { .init(type: .image, subType: "bmp") }
    /// Graphics Interchange Format (GIF)
    public static var imageGif: Self { .init(type: .image, subType: "gif") }
    /// Icon format
    public static var imageIco: Self { .init(type: .image, subType: "vnd.microsoft.icon") }
    /// JPEG images
    public static var imageJpeg: Self { .init(type: .image, subType: "jpeg") }
    /// Portable Network Graphics
    public static var imagePng: Self { .init(type: .image, subType: "png") }
    /// Scalable Vector Graphics (SVG)
    public static var imageSvg: Self { .init(type: .image, subType: "svg+xml") }
    /// Tagged Image File Format (TIFF)
    public static var imageTiff: Self { .init(type: .image, subType: "tiff") }
    /// WEBP image
    public static var imageWebp: Self { .init(type: .image, subType: "webp") }

    // audio

    /// AAC audio
    public static var audioAac: Self { .init(type: .audio, subType: "aac") }
    /// Musical Instrument Digital Interface (MIDI)
    public static var audioMidi: Self { .init(type: .audio, subType: "midi") }
    /// MP3 audio
    public static var audioMpeg: Self { .init(type: .audio, subType: "mpeg") }
    /// OGG audio
    public static var audioOgg: Self { .init(type: .audio, subType: "ogg") }
    /// Waveform Audio Format
    public static var audioWave: Self { .init(type: .audio, subType: "wave") }
    /// WEBM audio
    public static var audioWebm: Self { .init(type: .audio, subType: "webm") }
    /// Opus audio
    public static var audioOpus: Self { .init(type: .audio, subType: "opus") }
    /// 3GPP audio/video container
    public static var audio3gp: Self { .init(type: .audio, subType: "3gpp") }
    /// 3GPP2 audio/video container
    public static var audio3g2: Self { .init(type: .audio, subType: "3gpp2") }

    // video

    /// AVI: Audio Video Interleave
    public static var videoMp4: Self { .init(type: .video, subType: "mp4") }
    /// MPEG Video
    public static var videoMpeg: Self { .init(type: .video, subType: "mpeg") }
    /// OGG video
    public static var videoOgg: Self { .init(type: .video, subType: "ogg") }
    /// MPEG transport stream
    public static var videoTs: Self { .init(type: .video, subType: "mp2t") }
    /// WEBM video
    public static var videoWebm: Self { .init(type: .video, subType: "webm") }
    /// 3GPP audio/video container
    public static var video3gp: Self { .init(type: .video, subType: "3gpp") }
    /// 3GPP2 audio/video container
    public static var video3g2: Self { .init(type: .video, subType: "3gpp2") }

    // font

    /// OpenType font
    public static var fontOtf: Self { .init(type: .font, subType: "otf") }
    /// TrueType Font
    public static var fontTtf: Self { .init(type: .font, subType: "ttf") }
    /// Web Open Font Format (WOFF)
    public static var fontWoff: Self { .init(type: .font, subType: "woff") }
    /// Web Open Font Format (WOFF)
    public static var fontWoff2: Self { .init(type: .font, subType: "woff2") }

    // multipart

    /// Multipart formdata
    public static var multipartForm: Self { .init(type: .multipart, subType: "form-data") }

    /// map from extension string to media type
    static let extensionMap: [FileExtension: MediaType] = [
        .aac: .audioAac,
        .abw: .applicationAbiWord,
        .arc: .applicationArc,
        .azw: .applicationAmzKindleEBook,
        .bin: .applicationBinary,
        .bmp: .imageBmp,
        .bz: .applicationBzip,
        .bz2: .applicationBzip2,
        .csh: .applicationCsh,
        .css: .textCss,
        .csv: .textCsv,
        .doc: .applicationMsword,
        .docx: .applicationDocx,
        .eot: .applicationEot,
        .epub: .applicationEpub,
        .gz: .applicationGzip,
        .gif: .imageGif,
        .htm: .textHtml,
        .html: .textHtml,
        .ico: .imageIco,
        .ics: .textICalendar,
        .jar: .applicationJar,
        .jpeg: .imageJpeg,
        .jpg: .imageJpeg,
        .js: .textJavascript,
        .json: .applicationJson,
        .jsonld: .applicationJsonLD,
        .mid: .audioMidi,
        .midi: .audioMidi,
        .mjs: .textJavascript,
        .mp3: .audioMpeg,
        .mp4: .videoMp4,
        .mpeg: .videoMpeg,
        .mpkg: .applicationMpkg,
        .odp: .applicationOdp,
        .ods: .applicationOds,
        .odt: .applicationOdt,
        .oga: .audioOgg,
        .ogv: .videoOgg,
        .ogx: .applicationOgg,
        .opus: .audioOpus,
        .otf: .fontOtf,
        .png: .imagePng,
        .pdf: .applicationPdf,
        .php: .applicationPhp,
        .ppt: .applicationPpt,
        .pptx: .applicationPptx,
        .rar: .applicationRar,
        .rtf: .applicationRtf,
        .sh: .applicationSh,
        .svg: .imageSvg,
        .swf: .applicationSwf,
        .tar: .applicationTar,
        .tif: .imageTiff,
        .tiff: .imageTiff,
        .ts: .videoTs,
        .ttf: .fontTtf,
        .txt: .textPlain,
        .vsd: .applicationVsd,
        .wasm: .applicationWasm,
        .wav: .audioWave,
        .weba: .audioWebm,
        .webm: .videoWebm,
        .webp: .imageWebp,
        .webmanifest: .applicationManifest,
        .woff: .fontWoff,
        .woff2: .fontWoff2,
        .xhtml: .applicationXhtml,
        .xls: .applicationXls,
        .xlsx: .applicationXlsx,
        .xml: .applicationXml,
        .zip: .applicationZip,
        .threeGP: .video3gp,
        .threeG2: .video3g2,
        .sevenZ: .application7z,
    ]
}

extension MediaType {
    // Allow matching media types in switch statements
    public static func ~= (_ lhs: Self, _ rhs: Self) -> Bool {
        rhs.isType(lhs)
    }
}
